Skip to content

Feature : Added text-based search feature#667

Open
Isha-upadhyay wants to merge 4 commits intoAOSSIE-Org:mainfrom
Isha-upadhyay:feat/text-based-image-search
Open

Feature : Added text-based search feature#667
Isha-upadhyay wants to merge 4 commits intoAOSSIE-Org:mainfrom
Isha-upadhyay:feat/text-based-image-search

Conversation

@Isha-upadhyay
Copy link

@Isha-upadhyay Isha-upadhyay commented Nov 27, 2025

This PR introduces a complete text-based search system to enhance image discovery within PictoPy.
Previously, search was limited to face search only; the text search bar in the navbar was non-functional.
With this PR, users can now search images seamlessly using tags, filename, metadata, and even month/year.

Demo: https://www.loom.com/share/bd608939c716492abf8db5265671e5ed

What’s Added-

  • Text search for tags, metadata, filename, month/year
  • New backend endpoint: GET /images/search?query=
  • Search logic in db_search_images()
  • Debounced search input in Navbar
  • New useImageSearch hook
  • Home page now displays search results

fixes #661

Summary by CodeRabbit

  • New Features

    • Added image text search (query + optional "tagged" filter) with frontend hook and API; integrated into main page to display search results and show contextual titles for text or face searches.
    • Debounced navbar search with a dedicated focus button and updated placeholder.
  • Refactor

    • Split search into explicit text vs. face search flows for clearer behavior.
  • Bug Fixes

    • Safer favorite toggle that verifies image existence and handles failures more robustly.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

📝 Walkthrough

Walkthrough

Adds end-to-end text-based image search: new DB search function and GET /images/search backend route; frontend API + hook; Redux split for text vs face searches; Navbar debounced text input and Home integration to show search results; face-search action renamed wiring updated.

Changes

Cohort / File(s) Summary
Backend — DB & Routes
backend/app/database/images.py, backend/app/routes/images.py
Added db_search_images(query: str, tagged: Optional[bool]) to search tags/metadata/path with LEFT JOINs and aggregate tags per image; added GET /images/search route that validates params and returns transformed image results; db_toggle_image_favourite_status now verifies existence before toggling.
Backend — OpenAPI
docs/backend/backend_python/openapi.json
Declared new GET /images/search endpoint with required query (minLength 1) and optional tagged param; responses reuse existing image schemas.
Frontend — API & Hook
frontend/src/api/api-functions/images.ts, frontend/src/hooks/useImageSearch.ts
Added searchImages(query, tagged?) calling /images/search and useImageSearch(query, enabled) hook wrapping the query with key ['images','search',query], enabled only when query non-empty.
Frontend — Redux slice
frontend/src/features/searchSlice.ts
Replaced startSearch with startTextSearch and startFaceSearch; added type ('text'
Frontend — UI integration
frontend/src/components/Navigation/Navbar/Navbar.tsx, frontend/src/pages/Home/Home.tsx
Navbar: local debounced input (500ms) dispatches startTextSearch, Escape clears and dispatches clearSearch, input focus control added. Home: uses useImageSearch, disables normal fetch during active search, merges loading/isSuccess logic, and sources images from search when active.
Frontend — Face search wiring
frontend/src/components/Dialog/FaceSearchDialog.tsx, frontend/src/components/WebCam/WebCamComponent.tsx
Replaced uses of startSearch with startFaceSearch in imports and dispatch calls to initiate face searches.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Navbar as Navbar UI
    participant Redux as Redux Store
    participant Hook as useImageSearch
    participant API as Frontend API
    participant Backend as Backend API
    participant DB as Database
    participant Home as Home Component

    User->>Navbar: Type query
    Navbar->>Navbar: Debounce 500ms
    Navbar->>Redux: dispatch startTextSearch(query)
    Redux-->>Navbar: searchState updated (type='text', query)

    Home->>Hook: useImageSearch(query, enabled=true)
    Hook->>API: searchImages(query)
    API->>Backend: GET /images/search?query=...
    Backend->>DB: db_search_images(query, tagged?)
    DB-->>Backend: aggregated image list
    Backend-->>API: 200 OK with images
    API-->>Hook: return results
    Hook-->>Home: provide searchData

    Home->>Redux: dispatch setImages(searchData)
    Redux-->>Home: images updated
    Home-->>User: Render "Search Results for 'query'"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rahulharpal1603

Poem

🐰 I hopped along the search-lit trail,
From Navbar keystrokes to the DB's tale,
Text and face now dance in separate lanes,
Results return like soft spring rains,
A joyful hop — the gallery sings! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature : Added text-based search feature' clearly summarizes the main change: implementing text-based search functionality across the application.
Linked Issues check ✅ Passed All coding requirements from issue #661 are met: backend endpoint GET /images/search, db_search_images function, debounced navbar input, searchSlice updates, and Home page integration.
Out of Scope Changes check ✅ Passed All changes directly support text-based search requirements from #661; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)

64-76: Close button hidden during face search.

The close button only appears when isSearchActive (text search), but the query image is also displayed during face search. This creates inconsistent UX where users cannot close the query image during face search.

Consider showing the close button whenever a query image exists:

-            {isSearchActive && (
+            {queryImage && (
              <button
                onClick={() => {
                  setSearchInput('');
                  dispatch(clearSearch());
                }}

Or adjust the condition to cover both search types:

-            {isSearchActive && (
+            {searchState.active && (
frontend/src/pages/Home/Home.tsx (1)

50-64: Missing error handling for text search.

The feedback handler only uses isError and error from the normal fetch query (lines 54-55), ignoring errors from the text search hook. If text search fails, users won't see any error feedback.

Include text search error states:

+  const {
+    data: searchData,
+    isLoading: searchLoading,
+    isSuccess: searchSuccess,
+    isError: searchError,
+    error: searchErrorObj,
+  } = useImageSearch(searchQuery, isTextSearchActive);
+
   // LOADING STATUS
   const finalLoading = isTextSearchActive ? searchLoading : isLoading;
+  const finalError = isTextSearchActive ? searchError : isError;
+  const finalErrorObj = isTextSearchActive ? searchErrorObj : error;

   // FEEDBACK HANDLER
   useMutationFeedback(
     {
       isPending: finalLoading,
       isSuccess: isTextSearchActive ? searchSuccess : isSuccess,
-      isError,
-      error,
+      isError: finalError,
+      error: finalErrorObj,
     },
🧹 Nitpick comments (5)
frontend/src/api/api-functions/images.ts (1)

17-24: Use endpoint constant and improve type safety.

Two issues:

  1. The endpoint /images/search is hardcoded while fetchAllImages uses imagesEndpoints.getAllImages. Add a constant to apiEndpoints for consistency.
  2. Return type Promise<any> loses type safety. Consider using Promise<APIResponse> to match fetchAllImages.
+// In apiEndpoints.ts, add:
+// searchImages: '/images/search',

-export const searchImages = async (query: string, tagged?: boolean): Promise<any> => {
+export const searchImages = async (query: string, tagged?: boolean): Promise<APIResponse> => {
   const params = new URLSearchParams({ query });
   if (tagged !== undefined) {
     params.append('tagged', tagged.toString());
   }
-  const response = await apiClient.get(`/images/search?${params.toString()}`);
+  const response = await apiClient.get<APIResponse>(
+    `${imagesEndpoints.searchImages}?${params.toString()}`
+  );
   return response.data;
 };
frontend/src/hooks/useImageSearch.ts (1)

4-10: Consider adding staleTime to reduce refetch frequency.

The hook works correctly, but since search results are unlikely to change rapidly, adding staleTime could improve UX by reducing unnecessary API calls during typing (even with debouncing in the UI).

 export const useImageSearch = (query: string, enabled: boolean = true) => {
   return usePictoQuery({
     queryKey: ['images', 'search', query],
     queryFn: () => searchImages(query),
     enabled: enabled && query.length > 0,
+    staleTime: 30000, // 30 seconds
   });
 };
backend/app/routes/images.py (2)

112-120: Remove redundant validation.

This manual check is unnecessary since Query(..., min_length=1) on line 98 already ensures the query is non-empty. FastAPI will return a 422 validation error before this code executes if the query is empty or whitespace-only (after strip).

     try:
-        if not query or not query.strip():
-            raise HTTPException(
-                status_code=status.HTTP_400_BAD_REQUEST,
-                detail=ErrorResponse(
-                    success=False,
-                    error="Validation Error",
-                    message="Search query cannot be empty",
-                ).model_dump(),
-            )
-
         images = db_search_images(query.strip(), tagged=tagged)

124-136: Metadata is being parsed twice.

db_search_images (in backend/app/database/images.py, line 468) already calls image_util_parse_metadata(metadata) and stores the parsed dict in images_dict[image_id]["metadata"]. Calling it again here on line 130 will pass an already-parsed dict through the function.

While image_util_parse_metadata handles dict input gracefully (returns it as-is), this is inefficient and potentially confusing. Either remove the call here or document the double-parse behavior.

         image_data = [
             ImageData(
                 id=image["id"],
                 path=image["path"],
                 folder_id=image["folder_id"],
                 thumbnailPath=image["thumbnailPath"],
-                metadata=image_util_parse_metadata(image["metadata"]),
+                metadata=image["metadata"],  # Already parsed in db_search_images
                 isTagged=image["isTagged"],
                 isFavourite=image.get("isFavourite", False),
                 tags=image["tags"],
             )
             for image in images
         ]
backend/app/database/images.py (1)

455-465: Unused location_name variable.

location_name is fetched from the query results (line 427, 464) but is never used in the function body. Either remove it from the SELECT clause or use it (e.g., for enriching search results).

-        for (
-            image_id,
-            path,
-            folder_id,
-            thumbnail_path,
-            metadata,
-            is_tagged,
-            is_favourite,
-            tag_name,
-            location_name,
-        ) in results:
+        for (
+            image_id,
+            path,
+            folder_id,
+            thumbnail_path,
+            metadata,
+            is_tagged,
+            is_favourite,
+            tag_name,
+            _location_name,  # Used in WHERE clause only
+        ) in results:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c37d8df and dbe8c78.

📒 Files selected for processing (10)
  • backend/app/database/images.py (2 hunks)
  • backend/app/routes/images.py (2 hunks)
  • docs/backend/backend_python/openapi.json (1 hunks)
  • frontend/src/api/api-functions/images.ts (1 hunks)
  • frontend/src/components/Dialog/FaceSearchDialog.tsx (2 hunks)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (4 hunks)
  • frontend/src/components/WebCam/WebCamComponent.tsx (2 hunks)
  • frontend/src/features/searchSlice.ts (1 hunks)
  • frontend/src/hooks/useImageSearch.ts (1 hunks)
  • frontend/src/pages/Home/Home.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
frontend/src/hooks/useImageSearch.ts (2)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoQuery (80-108)
frontend/src/api/api-functions/images.ts (1)
  • searchImages (17-24)
frontend/src/components/WebCam/WebCamComponent.tsx (1)
frontend/src/features/searchSlice.ts (1)
  • startFaceSearch (29-34)
backend/app/routes/images.py (2)
backend/app/database/images.py (1)
  • db_search_images (399-499)
backend/app/utils/images.py (1)
  • image_util_parse_metadata (496-513)
frontend/src/api/api-functions/images.ts (1)
frontend/src/api/axiosConfig.ts (1)
  • apiClient (5-12)
frontend/src/components/Dialog/FaceSearchDialog.tsx (1)
frontend/src/features/searchSlice.ts (1)
  • startFaceSearch (29-34)
frontend/src/pages/Home/Home.tsx (6)
frontend/src/features/imageSelectors.ts (1)
  • selectImages (5-7)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoQuery (80-108)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/hooks/useImageSearch.ts (1)
  • useImageSearch (4-10)
frontend/src/types/Media.ts (1)
  • Image (13-23)
frontend/src/features/imageSlice.ts (1)
  • setImages (18-20)
backend/app/database/images.py (1)
backend/app/utils/images.py (1)
  • image_util_parse_metadata (496-513)
🔇 Additional comments (13)
frontend/src/features/searchSlice.ts (1)

1-45: LGTM! Clean separation of text and face search states.

The refactored slice correctly distinguishes between text and face search modes. Each action properly clears the other search type's data, preventing stale state conflicts.

frontend/src/components/Dialog/FaceSearchDialog.tsx (2)

14-14: LGTM!

Import correctly updated to use startFaceSearch for the face detection search flow.


86-86: LGTM!

Dispatch correctly uses startFaceSearch for face-based image search, aligning with the refactored search slice.

docs/backend/backend_python/openapi.json (1)

890-973: LGTM!

The OpenAPI specification for the new /images/search endpoint is well-documented with proper parameter validation, response schemas, and a clear description of limitations (face clusters not supported).

frontend/src/components/WebCam/WebCamComponent.tsx (2)

14-14: LGTM!

Import correctly updated for the face search action.


82-82: LGTM!

Correctly dispatches startFaceSearch for webcam-captured face searches, consistent with the refactored search slice.

backend/app/database/images.py (2)

414-414: Consider escaping SQL LIKE wildcards in user input.

The search pattern f"%{query}%" doesn't escape % and _ characters in the user's query. If a user searches for "100%", it will match "100" followed by any characters, not the literal "100%". For a search feature, this may be acceptable behavior, but it's worth documenting.

If literal wildcard matching is desired, escape them:

def escape_like(query: str) -> str:
    return query.replace('%', '\\%').replace('_', '\\_')

search_pattern = f"%{escape_like(query)}%"

And add ESCAPE '\\' to each LIKE clause in the query.


399-499: I encountered a repository cloning failure that prevents me from accessing the codebase to verify the reviewer's concern. Without access to the database schema files, I cannot definitively determine whether the metadata table exists or validate the query's correctness.

Given this limitation and the nature of the concern (a potential runtime failure if the table is missing), I must output the review comment with a verification tag indicating that manual investigation is needed:


Verify the metadata table exists in the database schema.

The query joins LEFT JOIN metadata md ON i.id = md.image_id, but the reviewer could not locate a metadata table creation in the db_create_images_table function. If this table doesn't exist in the database schema, the query will fail at runtime.

frontend/src/components/Navigation/Navbar/Navbar.tsx (2)

80-93: LGTM!

The search input implementation is solid: controlled state, clear placeholder text, and good keyboard accessibility with Escape key support.


18-18: Good separation of text and face search states.

The type-specific check properly distinguishes text search from face search, enabling different UI behaviors for each search mode.

frontend/src/pages/Home/Home.tsx (3)

26-30: Clean search state management.

The type-specific flags (isTextSearchActive, isFaceSearchActive) properly separate different search modes, and the searchQuery fallback prevents undefined issues.


32-44: Proper query coordination.

Disabling the normal fetch during active searches (line 36) prevents redundant API calls, and React Query will automatically refetch when the search clears. The text search hook is correctly gated by isTextSearchActive.


86-90: Excellent user feedback with dynamic title.

The title clearly communicates the search context and result count, providing immediate feedback to users about what they're viewing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
frontend/src/components/Navigation/Navbar/Navbar.tsx (3)

16-21: Debounced effect is coupled to searchState and can be simplified

Right now the effect depends on searchState.active / searchState.type but only uses them to gate clearSearch, which can:

  • Dispatch startTextSearch twice per new query (once due to searchInput change, once after active/type flip).
  • Auto‑clear a text search started elsewhere if searchInput is still '' when searchState flips to active.

If the intent is “Navbar owns text search and clearSearch is always safe when the input is empty”, you can decouple from searchState entirely and drop isSearchActive:

-  const isSearchActive = searchState.active && searchState.type === 'text';
+  // const isSearchActive = searchState.active && searchState.type === 'text';

   const dispatch = useDispatch();
   const [searchInput, setSearchInput] = useState('');

-  // Debounce search
-  useEffect(() => {
-    const timer = setTimeout(() => {
-      if (searchInput.trim().length > 0) {
-        dispatch(startTextSearch(searchInput.trim()));
-      } else if (searchInput.trim().length === 0 && isSearchActive) {
-        dispatch(clearSearch());
-      }
-    }, 500);
-
-    return () => clearTimeout(timer);
-
-    // use searchState
-  }, [searchInput, dispatch, searchState.active, searchState.type]);
+  // Debounce search based only on local input
+  useEffect(() => {
+    const trimmed = searchInput.trim();
+    const timer = setTimeout(() => {
+      if (trimmed) {
+        dispatch(startTextSearch(trimmed));
+      } else {
+        dispatch(clearSearch());
+      }
+    }, 500);
+    return () => clearTimeout(timer);
+  }, [searchInput, dispatch]);

This keeps behavior predictable (one dispatch per logical change, no hidden coupling to external text-search triggers).

Also applies to: 23-37


54-67: Consider whether Navbar should expose a way to clear the query image

The query image preview looks good, and the queryImage && ... guard keeps startsWith safe. However, with the inline close control removed, there’s no obvious way in the Navbar itself to “drop” the face/query image while staying on the same view.

If FaceSearchDialog is the only place to clear it, that’s fine; otherwise, consider re‑adding a small close/clear affordance on the thumbnail so users can quickly revert to a pure text search.


69-81: Wire up the Search icon button or make it non-interactive

The text input is already fully controlled and debounced, but the icon button is rendered as a <button> with no onClick, so it appears interactive yet does nothing. That’s confusing and not great for accessibility.

Either:

  • Attach behavior (e.g., focus the input or immediately trigger a search), or
  • Change it to a non-interactive element (e.g., <span> with appropriate classes and aria-hidden) so it’s clearly decorative.

Also applies to: 84-93

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbe8c78 and 30d392e.

📒 Files selected for processing (2)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (3 hunks)
  • frontend/src/pages/Home/Home.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/pages/Home/Home.tsx
🔇 Additional comments (1)
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)

1-4: Imports cleanly wire text search and icon usage

The new React/Redux imports plus startTextSearch/clearSearch and Search icon wiring are consistent with the rest of the file and the new text-search flow.

Also applies to: 7-7

@Isha-upadhyay Isha-upadhyay force-pushed the feat/text-based-image-search branch from 30d392e to a477c36 Compare February 4, 2026 16:12
@github-actions github-actions bot added backend enhancement New feature or request frontend labels Feb 4, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@backend/app/database/images.py`:
- Around line 417-437: The query in base_query incorrectly LEFT JOINs the
application-level metadata table (alias md) which has no image_id or location
columns, causing SQL errors; update the SELECT/WHERE to remove the LEFT JOIN
metadata md and instead search the JSON metadata stored in the images table
(images.metadata) directly (e.g., replace references to md.location with a
JSON/text search against images.metadata), leaving joins to image_classes (ic)
and mappings (m) intact; adjust the parameter bindings for the WHERE clause to
use images.metadata and ensure any selected fields no longer reference md or
md.location.

In `@frontend/src/components/Navigation/Navbar/Navbar.tsx`:
- Around line 66-78: The Input component is missing the ref assignment so the
declared inputRef isn't wired and the focus button cannot focus the field; pass
the inputRef to the Input component via its ref prop (i.e., <Input
ref={inputRef} ...>) where the JSX currently renders Input, keeping the existing
props (type, placeholder, className, value, onChange, onKeyDown) intact so the
focus handler that references inputRef and the dispatch(clearSearch()) logic
continue to work.

In `@frontend/src/pages/Home/Home.tsx`:
- Around line 67-84: The useEffect in Home.tsx is missing reactive dependencies
causing stale runs; update the dependency array for the effect that dispatches
setImages so it includes all booleans and state used in the callback
(isTextSearchActive, searchSuccess, searchState.active, isSuccess) as well as
searchData, data and dispatch so the effect re-runs when those values change;
locate the useEffect block using the useEffect(...) that references
isTextSearchActive, searchSuccess, searchData, searchState.active, isSuccess,
data, dispatch and add the missing variables to its dependency list.
🧹 Nitpick comments (5)
frontend/src/hooks/useImageSearch.ts (1)

4-9: Consider exposing the tagged filter parameter.

The searchImages API function supports an optional tagged parameter (see frontend/src/api/api-functions/images.ts lines 16-23), but this hook doesn't expose it. This limits search filtering capabilities.

♻️ Proposed enhancement to support tagged filter
-export const useImageSearch = (query: string, enabled: boolean = true) => {
+export const useImageSearch = (query: string, enabled: boolean = true, tagged?: boolean) => {
   return usePictoQuery({
-    queryKey: ['images', 'search', query],
-    queryFn: () => searchImages(query),
+    queryKey: ['images', 'search', query, tagged],
+    queryFn: () => searchImages(query, tagged),
     enabled: enabled && query.length > 0,
   });
 };
backend/app/database/images.py (2)

455-465: Unused variable location_name in result unpacking.

The location_name is unpacked from results (line 464) but never used in the image dictionary construction. Either use it or remove it from the SELECT and unpacking.

♻️ Either use location_name or remove it

If keeping location search but not the separate variable:

         for (
             image_id,
             path,
             folder_id,
             thumbnail_path,
             metadata,
             is_tagged,
             is_favourite,
             tag_name,
-            location_name,
+            _,  # location_name not needed separately
         ) in results:

414-414: Performance consideration: Leading wildcard prevents index usage.

The search pattern f"%{query}%" with leading wildcard will cause full table scans. For large image collections, this could be slow.

Consider documenting this limitation or exploring alternatives like FTS (Full-Text Search) for future scalability, as mentioned in the PR objectives.

frontend/src/components/Navigation/Navbar/Navbar.tsx (1)

21-34: Debounce effect looks correct but consider clearing search on unmount.

The debounce implementation is clean. However, if the component unmounts while a search is active, the search state remains in Redux. Consider clearing search on unmount if that's the desired UX.

♻️ Optional: Clear search on unmount
   useEffect(() => {
     const trimmed = searchInput.trim();

     const timer = setTimeout(() => {
       if (trimmed) {
         dispatch(startTextSearch(trimmed));
       } else {
         dispatch(clearSearch());
       }
     }, 500);

-    return () => clearTimeout(timer);
+    return () => {
+      clearTimeout(timer);
+    };
   }, [searchInput, dispatch]);
+
+  // Optional: Clear search when navigating away
+  useEffect(() => {
+    return () => {
+      dispatch(clearSearch());
+    };
+  }, [dispatch]);
frontend/src/pages/Home/Home.tsx (1)

80-83: Add Array.isArray validation for normal fetch data.

The text search path (lines 70-74) validates that the data is an array, but the normal fetch path doesn't have the same validation.

♻️ Proposed fix for consistency
     // No search → normal image fetch
     if (!searchState.active && isSuccess) {
       const images = (data?.data || []) as Image[];
+      if (!Array.isArray(images)) {
+        console.error("Invalid data format");
+        return;
+      }
       dispatch(setImages(images));
     }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@backend/app/database/images.py`:
- Around line 416-479: The current query (base_query) filters on m.name directly
which causes tag-only searches to return only the matching tag(s); replace this
with a two-step CTE approach: first build a CTE (e.g., matching_images) that
selects image ids matching the search criteria (use the same joins/WHERE logic
used now, including the optional i.isTagged filter) and then in the main query
select image columns and LEFT JOIN mappings/m to fetch all tags for those image
ids, ordering as before; update the SQL in base_query and the params list to
match the new CTE + main query structure while leaving the result-grouping logic
(the loop that builds images_dict and appends tag_name) unchanged so all tags
for matched images are returned.

In `@frontend/src/components/Navigation/Navbar/Navbar.tsx`:
- Around line 84-88: In the Navbar component's icon-only focus button (the
button that calls inputRef.current?.focus()), add an accessible name by adding
aria-label="Focus search" and set type="button" to prevent accidental form
submissions; update the button element in Navbar.tsx accordingly so screen
readers receive a reliable label and the button won't submit forms.
- Around line 16-34: Replace the loose any type in the selector with the app
RootState to restore type safety: import RootState from '@/app/store' (same
module used elsewhere) and update the selector call that reads queryImage (the
useSelector around state.search.queryImage) to use RootState as the state type
parameter so it becomes useSelector((state: RootState) =>
state.search.queryImage); keep dispatch and other hooks unchanged.
🧹 Nitpick comments (1)
backend/app/database/images.py (1)

414-443: Upstream validation already prevents empty queries at the route level.

The search_images endpoint in backend/app/routes/images.py validates the query parameter with Query(..., min_length=1) and explicitly checks if not query or not query.strip() before calling db_search_images(). This guard prevents "%%" from reaching the database function.

However, adding input validation at the database layer is still a reasonable defensive measure if the function might be called from other code paths outside the route. If the function is intended as an internal database utility only, the existing route-level validation is sufficient.

Comment on lines +416 to +479
# FIXED QUERY — removed face_clusters (they do NOT exist in DB)
base_query = """
SELECT DISTINCT
i.id,
i.path,
i.folder_id,
i.thumbnailPath,
i.metadata,
i.isTagged,
i.isFavourite,
m.name as tag_name
FROM images i
LEFT JOIN image_classes ic ON i.id = ic.image_id
LEFT JOIN mappings m ON ic.class_id = m.class_id
WHERE (
m.name LIKE ? OR
i.metadata LIKE ? OR
i.path LIKE ?
)
"""

params = [search_pattern, search_pattern, search_pattern]

# Optional filter
if tagged is not None:
base_query += " AND i.isTagged = ?"
params.append(tagged)

base_query += " ORDER BY i.path, m.name"

cursor.execute(base_query, params)
results = cursor.fetchall()

# Group results into image format
images_dict = {}
from app.utils.images import image_util_parse_metadata

for (
image_id,
path,
folder_id,
thumbnail_path,
metadata,
is_tagged,
is_favourite,
tag_name,
) in results:

if image_id not in images_dict:
metadata_dict = image_util_parse_metadata(metadata)

images_dict[image_id] = {
"id": image_id,
"path": path,
"folder_id": str(folder_id),
"thumbnailPath": thumbnail_path,
"metadata": metadata_dict,
"isTagged": bool(is_tagged),
"isFavourite": bool(is_favourite),
"tags": [],
}

if tag_name and tag_name not in images_dict[image_id]["tags"]:
images_dict[image_id]["tags"].append(tag_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and examine the surrounding context
find . -type f -name "images.py" | grep -E "database|app" | head -5

Repository: AOSSIE-Org/PictoPy

Length of output: 187


🏁 Script executed:

# Check for database schema files and migrations
find . -type f \( -name "*.sql" -o -name "*migration*" -o -name "*schema*" \) | head -20

Repository: AOSSIE-Org/PictoPy

Length of output: 44


🏁 Script executed:

# Search for table definitions related to images, image_classes, mappings
rg -t py "CREATE TABLE|class.*images|class.*mappings" --no-heading -i | head -30

Repository: AOSSIE-Org/PictoPy

Length of output: 2252


🏁 Script executed:

# Read the images table creation and the search function
cat -n backend/app/database/images.py | head -100

Repository: AOSSIE-Org/PictoPy

Length of output: 3220


🏁 Script executed:

# Check the mappings table structure
cat -n backend/app/database/yolo_mapping.py | grep -A 20 "CREATE TABLE"

Repository: AOSSIE-Org/PictoPy

Length of output: 785


🏁 Script executed:

# Read the complete search function around lines 416-479
sed -n '400,490p' backend/app/database/images.py | cat -n

Repository: AOSSIE-Org/PictoPy

Length of output: 3310


🏁 Script executed:

# Let's also find the function name to understand context
sed -n '350,430p' backend/app/database/images.py | cat -n

Repository: AOSSIE-Org/PictoPy

Length of output: 2898


Tag-only search results omit non-matching tags.

The WHERE clause filtering on m.name causes images found by tag search to return only the matching tag(s), while metadata/path matches return all tags because the WHERE conditions apply to the image level. This creates inconsistent tag lists depending on search type. Use a CTE to identify matching images first, then fetch all tags for those images:

Proposed fix (CTE to ensure complete tag lists)
-        base_query = """
-            SELECT DISTINCT
-                i.id,
-                i.path,
-                i.folder_id,
-                i.thumbnailPath,
-                i.metadata,
-                i.isTagged,
-                i.isFavourite,
-                m.name as tag_name
-            FROM images i
-            LEFT JOIN image_classes ic ON i.id = ic.image_id
-            LEFT JOIN mappings m ON ic.class_id = m.class_id
-            WHERE (
-                m.name LIKE ? OR
-                i.metadata LIKE ? OR
-                i.path LIKE ?
-            )
-        """
+        base_query = """
+            WITH matched AS (
+                SELECT DISTINCT i.id
+                FROM images i
+                LEFT JOIN image_classes ic ON i.id = ic.image_id
+                LEFT JOIN mappings m ON ic.class_id = m.class_id
+                WHERE (
+                    m.name LIKE ? OR
+                    i.metadata LIKE ? OR
+                    i.path LIKE ?
+                )
+            )
+            SELECT
+                i.id,
+                i.path,
+                i.folder_id,
+                i.thumbnailPath,
+                i.metadata,
+                i.isTagged,
+                i.isFavourite,
+                m.name as tag_name
+            FROM images i
+            JOIN matched mt ON i.id = mt.id
+            LEFT JOIN image_classes ic ON i.id = ic.image_id
+            LEFT JOIN mappings m ON ic.class_id = m.class_id
+            WHERE 1 = 1
+        """
🤖 Prompt for AI Agents
In `@backend/app/database/images.py` around lines 416 - 479, The current query
(base_query) filters on m.name directly which causes tag-only searches to return
only the matching tag(s); replace this with a two-step CTE approach: first build
a CTE (e.g., matching_images) that selects image ids matching the search
criteria (use the same joins/WHERE logic used now, including the optional
i.isTagged filter) and then in the main query select image columns and LEFT JOIN
mappings/m to fetch all tags for those image ids, ordering as before; update the
SQL in base_query and the params list to match the new CTE + main query
structure while leaving the result-grouping logic (the loop that builds
images_dict and appends tag_name) unchanged so all tags for matched images are
returned.

Comment on lines +16 to +34
const queryImage = useSelector((state: any) => state.search.queryImage);
const dispatch = useDispatch();
const [searchInput, setSearchInput] = useState('');
const inputRef = useRef<HTMLInputElement>(null);

const searchState = useSelector((state: any) => state.search);
const isSearchActive = searchState.active;
const queryImage = searchState.queryImage;
// Debounce search
useEffect(() => {
const trimmed = searchInput.trim();

const timer = setTimeout(() => {
if (trimmed) {
dispatch(startTextSearch(trimmed));
} else {
dispatch(clearSearch());
}
}, 500);

return () => clearTimeout(timer);
}, [searchInput, dispatch]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd frontend && find . -type f \( -name "*.ts" -o -name "*.tsx" \) | head -20

Repository: AOSSIE-Org/PictoPy

Length of output: 719


🏁 Script executed:

cd frontend && rg -l "RootState" --max-count=20

Repository: AOSSIE-Org/PictoPy

Length of output: 532


🏁 Script executed:

cd frontend && rg -l "useAppSelector" --max-count=20

Repository: AOSSIE-Org/PictoPy

Length of output: 44


🏁 Script executed:

cd frontend && find . -type f -name "store.ts" -o -name "store.js" -o -name "*store*" | grep -E "(store|redux|config)" | head -10

Repository: AOSSIE-Org/PictoPy

Length of output: 80


🏁 Script executed:

cd frontend && rg -n "export.*RootState\|type RootState" -A 2 --max-count=5

Repository: AOSSIE-Org/PictoPy

Length of output: 44


🏁 Script executed:

cat src/app/store.ts

Repository: AOSSIE-Org/PictoPy

Length of output: 110


🏁 Script executed:

cd frontend && cat -n src/components/Navigation/Navbar/Navbar.tsx | sed -n '1,40p'

Repository: AOSSIE-Org/PictoPy

Length of output: 1824


🏁 Script executed:

cd frontend && rg -A 3 "useSelector.*RootState" src/components --max-count=3

Repository: AOSSIE-Org/PictoPy

Length of output: 385


🏁 Script executed:

cat frontend/src/app/store.ts

Repository: AOSSIE-Org/PictoPy

Length of output: 1041


🏁 Script executed:

cd frontend && rg -n "import.*RootState" src/components/FaceCollections.tsx

Repository: AOSSIE-Org/PictoPy

Length of output: 104


🏁 Script executed:

cd frontend && cat -n src/features/searchSlice.ts | head -50

Repository: AOSSIE-Org/PictoPy

Length of output: 1505


Replace any with RootState in the search selector.

Line 16 uses state: any, which removes type safety and makes assumptions about the state shape fragile. The project exports RootState from @/app/store, which should be used here for consistency with other components like FaceCollections.tsx.

-  const queryImage = useSelector((state: any) => state.search.queryImage);
+  const queryImage = useSelector((state: RootState) => state.search.queryImage);

Add the import:

+import { RootState } from '@/app/store';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const queryImage = useSelector((state: any) => state.search.queryImage);
const dispatch = useDispatch();
const [searchInput, setSearchInput] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const searchState = useSelector((state: any) => state.search);
const isSearchActive = searchState.active;
const queryImage = searchState.queryImage;
// Debounce search
useEffect(() => {
const trimmed = searchInput.trim();
const timer = setTimeout(() => {
if (trimmed) {
dispatch(startTextSearch(trimmed));
} else {
dispatch(clearSearch());
}
}, 500);
return () => clearTimeout(timer);
}, [searchInput, dispatch]);
const queryImage = useSelector((state: RootState) => state.search.queryImage);
const dispatch = useDispatch();
const [searchInput, setSearchInput] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
// Debounce search
useEffect(() => {
const trimmed = searchInput.trim();
const timer = setTimeout(() => {
if (trimmed) {
dispatch(startTextSearch(trimmed));
} else {
dispatch(clearSearch());
}
}, 500);
return () => clearTimeout(timer);
}, [searchInput, dispatch]);
🤖 Prompt for AI Agents
In `@frontend/src/components/Navigation/Navbar/Navbar.tsx` around lines 16 - 34,
Replace the loose any type in the selector with the app RootState to restore
type safety: import RootState from '@/app/store' (same module used elsewhere)
and update the selector call that reads queryImage (the useSelector around
state.search.queryImage) to use RootState as the state type parameter so it
becomes useSelector((state: RootState) => state.search.queryImage); keep
dispatch and other hooks unchanged.

Comment on lines 84 to 88
<button
className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 cursor-pointer rounded-sm p-2"
title="Search"
aria-label="Search"
onClick={() => inputRef.current?.focus()}
className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 rounded-sm p-2"
title="Focus search"
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an accessible name to the icon-only focus button.

title isn’t reliably announced by screen readers; add aria-label (and set type="button" to avoid accidental form submits).

✅ Suggested fix
-          <button
+          <button
+            type="button"
+            aria-label="Focus search"
             onClick={() => inputRef.current?.focus()}
             className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 rounded-sm p-2"
             title="Focus search"
           >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 cursor-pointer rounded-sm p-2"
title="Search"
aria-label="Search"
onClick={() => inputRef.current?.focus()}
className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 rounded-sm p-2"
title="Focus search"
>
<button
type="button"
aria-label="Focus search"
onClick={() => inputRef.current?.focus()}
className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 rounded-sm p-2"
title="Focus search"
>
🤖 Prompt for AI Agents
In `@frontend/src/components/Navigation/Navbar/Navbar.tsx` around lines 84 - 88,
In the Navbar component's icon-only focus button (the button that calls
inputRef.current?.focus()), add an accessible name by adding aria-label="Focus
search" and set type="button" to prevent accidental form submissions; update the
button element in Navbar.tsx accordingly so screen readers receive a reliable
label and the button won't submit forms.

Copy link
Contributor

@rohan-pandeyy rohan-pandeyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've done a great job on this PR @Isha-upadhyay. There's one UX change I'd suggest you implement and it's that rather than applying a debounce functionality to the search feature, you should replace it with a "only dispatch when enter is pressed or search button is clicked" method. And incase a user clears out the text, (whether using the "x" button or clearing by backspace) it should automatically clear the search results.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make use of RootState inside @/app/store.ts and replace the Debounce search with a search handler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Implement text-based image search functionality

2 participants